/*
 * To change this template, choose JavaTool | Templates
 * and open the template in the editor.
 */
package yinshi.fsm;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import yinshi.util.JavaTool;

/**
 *
 * @author yinshi
 */
public class TextParser {

    public static final int FALSE_JUDGE = -1;
    public static final int EXIT_STATE_ID = -1;
    //
    private int entryStateId = 0;
    private final State[] states;

    public TextParser(int number) {
        if (number <= 0) {
            throw new IllegalArgumentException("number must be larger than zero");
        }
        states = new State[number];
    }

    public final State getState(int id) {
        if (states[id] == null) {
            State state = new State(this, id);
            states[id] = state;
            return state;
        } else {
            return states[id];
        }
    }

    public final void setEntryState(int id) {
        if ((id < 0) || (id >= states.length)) {
            throw new IndexOutOfBoundsException("illegal state id");
        }
        entryStateId = id;
    }

    public synchronized final int process(CharSequence text, int start) throws IOException {
        StringProxy sp = new StringProxy(text, start);
        State state = states[entryStateId];
        Result reObj = new Result();

        big_loop:
        while (true) {
            JavaTool.debug("Enter %s# state: (text = \"%s\")\n", state.id, sp);
            for (int index = 0; index < state.jumps.size(); index++) {
                Jump jump = state.jumps.get(index);

                int result = jump.cond.judge(state.id, index, sp, reObj);

                if (result == FALSE_JUDGE) {
                    JavaTool.debug("  Condition %d: false\n", index);
                    continue;
                }

                if (result >= 0) {
                    if (JavaTool.isDebug()) {
                        StringBuilder builder = new StringBuilder();
                        builder.append(sp, 0, result);
                        JavaTool.debug("  Condition %d: successful (result = %d, text = %s)\n", index, result, builder);
                        builder.delete(0, builder.length());
                    }

                    if (jump.handler != null) {
                        jump.handler.hit(state.id, jump.targetId, index, reObj);
                    }

                    if (jump.targetId == EXIT_STATE_ID) {
                        JavaTool.debug("  exit the parser\n");
                        break big_loop;
                    }

                    if (!jump.rollback) {
                        if (result >= sp.length()) {
                            JavaTool.debug("### String is exhausted.\n");
                            break big_loop;
                        }
                        sp.setStart(sp.start + result);
                    }
                    state = states[jump.targetId];
                    JavaTool.debug("  move to %s# state\n", state.id);
                    continue big_loop;
                } else {
                    throw new RuntimeException("Illegal result");
                }
            }
            if (state.defaultTargetId == EXIT_STATE_ID) {
                JavaTool.debug("  No matched condition and exit the parser\n");
                break big_loop;
            } else {
                state = states[state.defaultTargetId];
                JavaTool.debug("  No matched condition and move to %s# state\n", state.id);
            }
        }
        return sp.start - start;
    }

    public static final class State {

        private final ArrayList<Jump> jumps = new ArrayList<>();
        private final TextParser p;
        private final int id;
        private int defaultTargetId;

        private State(TextParser p, int id) {
            this.p = p;
            this.id = id;
        }

        public void setDefaultTarget(int id) {
            if ((id < EXIT_STATE_ID) || (id >= p.states.length)) {
                throw new IndexOutOfBoundsException("illegal state id");
            }
            this.defaultTargetId = id;
        }

        public int addJump(int targetId, Object cond, boolean rollback, Handler handler) {
            Objects.requireNonNull(cond);
            if ((targetId < EXIT_STATE_ID) || (targetId >= p.states.length)) {
                throw new IndexOutOfBoundsException("illegal target state id");
            }

            int index = jumps.size();
            if (cond instanceof Character) {
                jumps.add(new Jump(targetId, new CharCond((Character) cond), rollback, handler));
            } else if (cond instanceof char[]) {
                jumps.add(new Jump(targetId, new CharSetCond((char[]) cond), rollback, handler));
            } else if (cond instanceof String) {
                jumps.add(new Jump(targetId, new StrCond((String) cond), rollback, handler));
            } else if (cond instanceof String[]) {
                jumps.add(new Jump(targetId, new StrSetCond((String[]) cond), rollback, handler));
            } else if (cond instanceof Pattern) {
                jumps.add(new Jump(targetId, new RegexCondition((Pattern) cond), rollback, handler));
            } else if (cond instanceof Condition) {
                jumps.add(new Jump(targetId, (Condition) cond, rollback, handler));
            } else {
                jumps.add(new Jump(targetId, new StrCond(cond.toString()), rollback, handler));
            }

            return index;
        }
    }

    private static final class Jump {

        int targetId;
        Condition cond;
        boolean rollback;
        Handler handler;

        public Jump(int targetId, Condition cond, boolean rollback, Handler handler) {
            this.targetId = targetId;
            this.cond = cond;
            this.rollback = rollback;
            this.handler = handler;
        }
    }

    public static interface Condition {

        public int judge(int stateId, int jumpIndex, CharSequence text, Result reObj);
    }

    private static class RegexCondition implements Condition {

        private final Pattern pattern;

        private RegexCondition(Pattern pattern) {
            this.pattern = pattern;
        }

        public int judge(int stateId, int jumpIndex, CharSequence ch, Result reObj) {
            Matcher m = pattern.matcher(ch);
            if (m.lookingAt()) {
                return m.end();
            } else {
                return FALSE_JUDGE;
            }
        }
    }

    private static class CharCond implements Condition {

        private final char ch;

        private CharCond(char ch) {
            this.ch = ch;
        }

        public int judge(int stateId, int jumpIndex, CharSequence text, Result reObj) {
            if (text.charAt(0) == ch) {
                reObj.data = ch;
                return 1;
            } else {
                return FALSE_JUDGE;
            }
        }
    }

    private static class CharSetCond implements Condition {

        private final char[] charSet;

        private CharSetCond(char[] charSet) {
            this.charSet = charSet;
        }

        public int judge(int stateId, int jumpIndex, CharSequence ch, Result reObj) {
            for (int i = 0; i < charSet.length; i++) {
                if (ch.charAt(0) == charSet[i]) {
                    reObj.data = charSet[i];
                    return 1;
                }
            }
            return FALSE_JUDGE;
        }
    }

    private static class StrCond implements Condition {

        private final String str;

        private StrCond(String str) {
            this.str = str;
        }

        public int judge(int stateId, int jumpIndex, CharSequence ch, Result reObj) {
            if (str.length() <= ch.length()) {
                for (int i = 0; i < str.length(); i++) {
                    if (str.charAt(i) != ch.charAt(i)) {
                        return FALSE_JUDGE;
                    }
                }
                reObj.data = str;
                return str.length();
            }
            return FALSE_JUDGE;
        }
    }

    private static class StrSetCond implements Condition {

        private final String[] strings;

        private StrSetCond(String[] strings) {
            this.strings = strings;
        }

        public int judge(int stateId, int jumpIndex, CharSequence ch, Result reObj) {
            big_loop:
            for (String str : strings) {
                if (str.length() <= ch.length()) {
                    for (int i = 0; i < str.length(); i++) {
                        if (str.charAt(i) != ch.charAt(i)) {
                            continue big_loop;
                        }
                    }
                    reObj.data = str;
                    return str.length();
                }
            }
            return FALSE_JUDGE;
        }
    }

    public interface Handler {

        public abstract void hit(int stateId, int targetId, int jumpIndex, Result result);
    }

    public static class Result {

        public CharSequence text;
        public int start;
        public int end;
        public Object data;
    }

    private static final class StringProxy implements CharSequence {

        private final CharSequence text;
        private int start;

        public StringProxy(CharSequence text, int start) {
            assert text != null;
            this.text = text;
            this.start = start;
        }

        public void setStart(int start) {
            this.start = start;
        }

        public int length() {
            return text.length() - start;
        }

        public char charAt(int index) {
            return text.charAt(start + index);
        }

        public CharSequence subSequence(int start, int end) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(text, start, text.length());
            return sb.toString();
        }
    }
}
